import { Fragment } from "./fragment";
import { Mark } from "./mark";
import { Slice, replace } from "./replace";
import { ResolvedPos } from "./resolvedpos";
import { compareDeep } from "./comparedeep";

const emptyAttrs = Object.create(null);

// ::这个类表示构成ProseMirror文档的树上的一个节点。所以一个文档是`节点`的实例，其子类也是`节点`的实例。
//
// 节点是持久的数据结构。你不需要改变它们，而是用你想要的内容创建新的节点。
// 旧的一直指向旧的文档形状。通过在新旧数据之间尽可能地共享结构，这就使成本降低了
// ，像这样的树形（没有反向指针）就很容易实现。
//
// **Do not** directly mutate the properties of a `Node` object. See
// [the guide](/docs/guide/#doc) for more information.
export class Node {
    constructor(type, attrs, content, marks) {
        // :: NodeType
        // The type of node that this is.
        this.type = type;
        // console.log(this.type);
        // :: Object
        // An object mapping attribute names to values. The kind of
        // attributes allowed and required are
        // [determined](#model.NodeSpec.attrs) by the node type.
        this.attrs = attrs;
        // console.log(this.attrs);
        // :: Fragment
        // A container holding the node's children.
        this.content = content || Fragment.empty;
        // console.log(this.content);
        // :: [Mark]
        // The marks (things like whether it is emphasized or part of a
        // link) applied to this node.
        this.marks = marks || Mark.none;
        // console.log(this.marks);
    }

    // text:: ?string
    // For text nodes, this contains the node's text content.

    // :: number
    // The size of this node, as defined by the integer-based [indexing
    // scheme](/docs/guide/#doc.indexing). For text nodes, this is the
    // amount of characters. For other leaf nodes, it is one. For
    // non-leaf nodes, it is the size of the content plus two (the start
    // and end token).

    /**
     * 该节点的大小，由基于整数的[索引方案](/docs/guide/#doc.indexing)定义。
     * 对于文本节点，这是字符的数量。对于其他叶子节点，它是一个。对于非叶子节点，它是内容的大小加上二（开始和结束标记）。
     */
    get nodeSize() {
        return this.isLeaf ? 1 : 2 + this.content.size;
    }

    // :: number
    // The number of children that the node has.
    get childCount() {
        return this.content.childCount;
    }

    // :: (number) → Node
    // Get the child node at the given index. Raises an error when the
    // index is out of range.
    child(index) {
        return this.content.child(index);
    }

    // :: (number) → ?Node
    // Get the child node at the given index, if it exists.
    maybeChild(index) {
        return this.content.maybeChild(index);
    }

    // :: ((node: Node, offset: number, index: number))
    // Call `f` for every child node, passing the node, its offset
    // into this parent node, and its index.
    forEach(f) {
        this.content.forEach(f);
    }

    // :: (number, number, (node: Node, pos: number, parent: Node, index: number) → ?bool, ?number)
    // Invoke a callback for all descendant nodes recursively between
    // the given two positions that are relative to start of this node's
    // content. The callback is invoked with the node, its
    // parent-relative position, its parent node, and its child index.
    // When the callback returns false for a given node, that node's
    // children will not be recursed over. The last parameter can be
    // used to specify a starting position to count from.
    nodesBetween(from, to, f, startPos = 0) {
        this.content.nodesBetween(from, to, f, startPos, this);
    }

    // :: ((node: Node, pos: number, parent: Node) → ?bool)
    // Call the given callback for every descendant node. Doesn't
    // descend into a node when the callback returns `false`.
    descendants(f) {
        this.nodesBetween(0, this.content.size, f);
    }

    // :: string
    // Concatenates all the text nodes found in this fragment and its
    // children.
    get textContent() {
        return this.textBetween(0, this.content.size, "");
    }

    // :: (number, number, ?string, ?string) → string
    // Get all text between positions `from` and `to`. When
    // `blockSeparator` is given, it will be inserted whenever a new
    // block node is started. When `leafText` is given, it'll be
    // inserted for every non-text leaf node encountered.
    textBetween(from, to, blockSeparator, leafText) {
        return this.content.textBetween(from, to, blockSeparator, leafText);
    }

    // :: ?Node
    // Returns this node's first child, or `null` if there are no
    // children.
    get firstChild() {
        return this.content.firstChild;
    }

    // :: ?Node
    // Returns this node's last child, or `null` if there are no
    // children.
    get lastChild() {
        return this.content.lastChild;
    }

    // :: (Node) → bool
    // Test whether two nodes represent the same piece of document.
    eq(other) {
        return (
            this == other ||
            (this.sameMarkup(other) && this.content.eq(other.content))
        );
    }

    // :: (Node) → bool
    // Compare the markup (type, attributes, and marks) of this node to
    // those of another. Returns `true` if both have the same markup.
    sameMarkup(other) {
        return this.hasMarkup(other.type, other.attrs, other.marks);
    }

    // :: (NodeType, ?Object, ?[Mark]) → bool
    // Check whether this node's markup correspond to the given type,
    // attributes, and marks.
    hasMarkup(type, attrs, marks) {
        return (
            this.type == type &&
            compareDeep(this.attrs, attrs || type.defaultAttrs || emptyAttrs) &&
            Mark.sameSet(this.marks, marks || Mark.none)
        );
    }

    // :: (?Fragment) → Node
    // Create a new node with the same markup as this node, containing
    // the given content (or empty, if no content is given).
    copy(content = null) {
        if (content == this.content) return this;
        return new this.constructor(this.type, this.attrs, content, this.marks);
    }

    // :: ([Mark]) → Node
    // Create a copy of this node, with the given set of marks instead
    // of the node's own marks.
    mark(marks) {
        return marks == this.marks
            ? this
            : new this.constructor(this.type, this.attrs, this.content, marks);
    }

    // :: (number, ?number) → Node
    // Create a copy of this node with only the content between the
    // given positions. If `to` is not given, it defaults to the end of
    // the node.
    cut(from, to) {
        if (from == 0 && to == this.content.size) return this;
        return this.copy(this.content.cut(from, to));
    }

    // :: (number, ?number) → Slice
    // Cut out the part of the document between the given positions, and
    // return it as a `Slice` object.
    slice(from, to = this.content.size, includeParents = false) {
        if (from == to) return Slice.empty;

        let $from = this.resolve(from),
            $to = this.resolve(to);
        let depth = includeParents ? 0 : $from.sharedDepth(to);
        let start = $from.start(depth),
            node = $from.node(depth);
        let content = node.content.cut($from.pos - start, $to.pos - start);
        return new Slice(content, $from.depth - depth, $to.depth - depth);
    }

    // :: (number, number, Slice) → Node
    // Replace the part of the document between the given positions with
    // the given slice. The slice must 'fit', meaning its open sides
    // must be able to connect to the surrounding content, and its
    // content nodes must be valid children for the node they are placed
    // into. If any of this is violated, an error of type
    // [`ReplaceError`](#model.ReplaceError) is thrown.
    replace(from, to, slice) {
        return replace(this.resolve(from), this.resolve(to), slice);
    }

    // :: (number) → ?Node
    // Find the node directly after the given position.
    nodeAt(pos) {
        for (let node = this; ;) {
            let { index, offset } = node.content.findIndex(pos);
            node = node.maybeChild(index);
            if (!node) return null;
            if (offset == pos || node.isText) return node;
            pos -= offset + 1;
        }
    }

    // :: (number) → {node: ?Node, index: number, offset: number}
    // Find the (direct) child node after the given offset, if any,
    // and return it along with its index and offset relative to this
    // node.
    childAfter(pos) {
        let { index, offset } = this.content.findIndex(pos);
        return { node: this.content.maybeChild(index), index, offset };
    }

    // :: (number) → {node: ?Node, index: number, offset: number}
    // Find the (direct) child node before the given offset, if any,
    // and return it along with its index and offset relative to this
    // node.
    childBefore(pos) {
        if (pos == 0) return { node: null, index: 0, offset: 0 };
        let { index, offset } = this.content.findIndex(pos);
        if (offset < pos)
            return { node: this.content.child(index), index, offset };
        let node = this.content.child(index - 1);
        return { node, index: index - 1, offset: offset - node.nodeSize };
    }

    // :: (number) → ResolvedPos
    // Resolve the given position in the document, returning an
    // [object](#model.ResolvedPos) with information about its context.
    resolve(pos) {
        return ResolvedPos.resolveCached(this, pos);
    }

    resolveNoCache(pos) {
        return ResolvedPos.resolve(this, pos);
    }

    // :: (number, number, union<Mark, MarkType>) → bool
    // Test whether a given mark or mark type occurs in this document
    // between the two given positions.
    rangeHasMark(from, to, type) {
        let found = false;
        if (to > from)
            this.nodesBetween(from, to, (node) => {
                if (type.isInSet(node.marks)) found = true;
                return !found;
            });
        return found;
    }

    // :: bool
    // True when this is a block (non-inline node)
    get isBlock() {
        return this.type.isBlock;
    }

    // :: bool
    // True when this is a textblock node, a block node with inline
    // content.
    get isTextblock() {
        return this.type.isTextblock;
    }

    // :: bool
    // True when this node allows inline content.
    get inlineContent() {
        return this.type.inlineContent;
    }

    // :: bool
    // True when this is an inline node (a text node or a node that can
    // appear among text).
    get isInline() {
        return this.type.isInline;
    }

    // :: bool
    // True when this is a text node.
    get isText() {
        return this.type.isText;
    }

    // :: bool
    // True when this is a leaf node.
    get isLeaf() {
        return this.type.isLeaf;
    }

    // :: bool
    // True when this is an atom, i.e. when it does not have directly
    // editable content. This is usually the same as `isLeaf`, but can
    // be configured with the [`atom` property](#model.NodeSpec.atom) on
    // a node's spec (typically used when the node is displayed as an
    // uneditable [node view](#view.NodeView)).
    get isAtom() {
        return this.type.isAtom;
    }

    // :: () → string
    // Return a string representation of this node for debugging
    // purposes.
    toString() {
        if (this.type.spec.toDebugString)
            return this.type.spec.toDebugString(this);
        let name = this.type.name;
        if (this.content.size) name += "(" + this.content.toStringInner() + ")";
        return wrapMarks(this.marks, name);
    }

    // :: (number) → ContentMatch
    // Get the content match in this node at the given index.
    contentMatchAt(index) {
        let match = this.type.contentMatch.matchFragment(
            this.content,
            0,
            index
        );
        if (!match)
            throw new Error(
                "Called contentMatchAt on a node with invalid content"
            );
        return match;
    }

    // :: (number, number, ?Fragment, ?number, ?number) → bool
    // Test whether replacing the range between `from` and `to` (by
    // child index) with the given replacement fragment (which defaults
    // to the empty fragment) would leave the node's content valid. You
    // can optionally pass `start` and `end` indices into the
    // replacement fragment.
    canReplace(
        from,
        to,
        replacement = Fragment.empty,
        start = 0,
        end = replacement.childCount
    ) {
        let one = this.contentMatchAt(from).matchFragment(
            replacement,
            start,
            end
        );
        let two = one && one.matchFragment(this.content, to);
        if (!two || !two.validEnd) return false;
        for (let i = start; i < end; i++)
            if (!this.type.allowsMarks(replacement.child(i).marks))
                return false;
        return true;
    }

    // :: (number, number, NodeType, ?[Mark]) → bool
    // Test whether replacing the range `from` to `to` (by index) with a
    // node of the given type would leave the node's content valid.
    canReplaceWith(from, to, type, marks) {
        if (marks && !this.type.allowsMarks(marks)) return false;
        let start = this.contentMatchAt(from).matchType(type);
        let end = start && start.matchFragment(this.content, to);
        return end ? end.validEnd : false;
    }

    // :: (Node) → bool
    // Test whether the given node's content could be appended to this
    // node. If that node is empty, this will only return true if there
    // is at least one node type that can appear in both nodes (to avoid
    // merging completely incompatible nodes).
    canAppend(other) {
        if (other.content.size)
            return this.canReplace(
                this.childCount,
                this.childCount,
                other.content
            );
        else return this.type.compatibleContent(other.type);
    }

    // :: ()
    // Check whether this node and its descendants conform to the
    // schema, and raise error when they do not.
    check() {
        if (!this.type.validContent(this.content))
            throw new RangeError(
                `Invalid content for node ${this.type.name
                }: ${this.content.toString().slice(0, 50)}`
            );
        this.content.forEach((node) => node.check());
    }

    // :: () → Object
    // Return a JSON-serializeable representation of this node.
    toJSON() {
        let obj = { type: this.type.name };
        for (let _ in this.attrs) {
            obj.attrs = this.attrs;
            break;
        }
        if (this.content.size) obj.content = this.content.toJSON();
        if (this.marks.length) obj.marks = this.marks.map((n) => n.toJSON());
        return obj;
    }

    // :: (Schema, Object) → Node
    // Deserialize a node from its JSON representation.
    static fromJSON(schema, json) {
        if (!json) throw new RangeError("Invalid input for Node.fromJSON");
        let marks = null;
        if (json.marks) {
            if (!Array.isArray(json.marks))
                throw new RangeError("Invalid mark data for Node.fromJSON");
            marks = json.marks.map(schema.markFromJSON);
        }
        if (json.type == "text") {
            if (typeof json.text != "string")
                throw new RangeError("Invalid text node in JSON");
            return schema.text(json.text, marks);
        }
        let content = Fragment.fromJSON(schema, json.content);
        return schema.nodeType(json.type).create(json.attrs, content, marks);
    }
}

export class TextNode extends Node {
    constructor(type, attrs, content, marks) {
        super(type, attrs, null, marks);

        if (!content) throw new RangeError("Empty text nodes are not allowed");

        this.text = content;
    }

    toString() {
        if (this.type.spec.toDebugString)
            return this.type.spec.toDebugString(this);
        return wrapMarks(this.marks, JSON.stringify(this.text));
    }

    get textContent() {
        return this.text;
    }

    textBetween(from, to) {
        return this.text.slice(from, to);
    }

    get nodeSize() {
        return this.text.length;
    }

    mark(marks) {
        return marks == this.marks
            ? this
            : new TextNode(this.type, this.attrs, this.text, marks);
    }

    withText(text) {
        if (text == this.text) return this;
        return new TextNode(this.type, this.attrs, text, this.marks);
    }

    cut(from = 0, to = this.text.length) {
        if (from == 0 && to == this.text.length) return this;
        return this.withText(this.text.slice(from, to));
    }

    eq(other) {
        return this.sameMarkup(other) && this.text == other.text;
    }

    toJSON() {
        let base = super.toJSON();
        base.text = this.text;
        return base;
    }
}

function wrapMarks(marks, str) {
    for (let i = marks.length - 1; i >= 0; i--)
        str = marks[i].type.name + "(" + str + ")";
    return str;
}
